﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 自定义定时任务.Common
{
    public class CronExpressionParser
    {
        private readonly string[] _expressionParts;

        public CronExpressionParser(string cronExpression)
        {
            if (string.IsNullOrWhiteSpace(cronExpression))
                throw new ArgumentException("Cron expression cannot be empty");

            var parts = cronExpression.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length != 6)
                throw new FormatException("Invalid cron expression format");

            _expressionParts = parts;
        }


        public DateTime GetNextOccurrence(DateTime fromTime)
        {
            var next = fromTime.AddSeconds(1); // 至少1秒后
            while (true)
            {
                // 秒 分 时 日 月 周
                // 30 0/5 1,2,3  *  1-2  *
                // 下面的这段逻辑的执行原理就是：
                //      尽可能地去匹配更多符合条件的项，直到遇到不满足条件的项为止，
                //      然后在当前时间的基础之上加1分钟，再去判断是否符合条件，
                //      当全部都符合条件后就返回当前的时间作为下一次的事件发生时间
                // 注意：
                //      这里的匹配顺序是倒着匹配的，先从最大的时间单位去匹配，
                // 总的来说：
                //      就是通过最小时间点累加，等所有的条件全部满足的时候返回结果
                if (!MatchesYear(next) || !MatchesMonth(next) || !MatchesDay(next) || !MatchesWeekday(next) || !MatchesHour(next) || !MatchesMinute(next))
                {
                    next = next.AddMinutes(1);
                    continue;
                }
                return new DateTime(next.Year, next.Month, next.Day, next.Hour, next.Minute, 0);
            }
        }

        #region 时间类型匹配
        private bool MatchesMinute(DateTime time)
        {
            return MatchesPart(_expressionParts[0], time.Minute, 0, 59);
        }

        private bool MatchesHour(DateTime time)
        {
            return MatchesPart(_expressionParts[1], time.Hour, 0, 23);
        }

        private bool MatchesDay(DateTime time)
        {
            return MatchesPart(_expressionParts[2], time.Day, 1, 31);
        }

        private bool MatchesMonth(DateTime time)
        {
            return MatchesPart(_expressionParts[3], time.Month, 1, 12);
        }

        private bool MatchesWeekday(DateTime time)
        {
            return MatchesPart(_expressionParts[4], (int)time.DayOfWeek, 0, 6);
        }

        private bool MatchesYear(DateTime time)
        {
            if (_expressionParts.Length == 7)
                return MatchesPart(_expressionParts[5], time.Year, 1970, 2099);
            return true;
        }

        private bool MatchesPart(string part, int value, int min, int max)
        {
            if (part == "*") return true;
            if (part.Contains(","))     // 1,2,3,4
            {
                return part.Split(',').Any(p => MatchesPart(p, value, min, max));
            }
            if (part.Contains("-"))     // 1-4
            {
                var range = part.Split('-');
                if (range.Length != 2)
                    throw new Exception("cron表达式格式错误，这种类型的格式1-5，不能写出 1- 或 -5 这种形式");
                int start = ParseNumber(range[0], min, max);
                int end = ParseNumber(range[1], min, max);
                return value >= start && value <= end;
            }
            if (part.Contains("/"))     // 0/5 从0秒开始，每隔5秒触发一次
            {
                var div = part.Split('/');
                if (div.Length != 2)
                    throw new Exception("cron表达式格式错误，这种类型的格式0/5,不能写成 0/ 或 /5 这种格式");
                int step = ParseNumber(div[1], 1, max);
                if (div[0] == "*")
                    return value % step == 0;
                int start = ParseNumber(div[0], min, max);
                // 适用于这种情况 30/5 以秒为例，时间大于30秒以后，每5秒执行一次
                return value >= start && (value - start) % step == 0;
            }

            return ParseNumber(part, min, max) == value;
        }
        #endregion


        private int ParseNumber(string numStr, int min, int max)
        {
            if (!int.TryParse(numStr, out int num))
                throw new FormatException($"Invalid number format: {numStr}");

            if (num < min || num > max)
                throw new FormatException($"Number {num} out of range [{min}-{max}]");

            return num;
        }
    }
}
